package com.ruoyi.oss.service.impl;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.extra.servlet.ServletUtil;
import com.qcloud.cos.COSClient;
import com.qcloud.cos.ClientConfig;
import com.qcloud.cos.auth.BasicSessionCredentials;
import com.qcloud.cos.exception.CosClientException;
import com.qcloud.cos.model.*;
import com.qcloud.cos.region.Region;
import com.qcloud.cos.utils.Jackson;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringPool;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.oss.config.properties.OssProperties;
import com.ruoyi.oss.domain.SysOss;
import com.ruoyi.oss.domain.bo.UploadResult;
import com.ruoyi.oss.ienum.OssType;
import com.ruoyi.oss.service.CloudOssService;
import com.tencent.cloud.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.TreeMap;

@Slf4j
public class QCloudOssCloudServiceImpl implements CloudOssService {

    private final OssProperties ossProperties;

    public QCloudOssCloudServiceImpl(OssProperties ossProperties) {
        this.ossProperties = ossProperties;
    }


    /**
     * 获取临时授权
     * <p>
     * 该授权拥有 上传 下载对象的权限 包含bucket下的所有目录
     *
     * @return Response
     * @throws IOException io异常
     */
    private Response getTempAuthorization() throws IOException {
        OssProperties.QCloudProperties qCloudProperties = ossProperties.getQCloud();
        String bucketName = qCloudProperties.getBucketName();
        String secretId = qCloudProperties.getSecretId();
        String secretKey = qCloudProperties.getSecretKey();
        TreeMap<String, Object> config = new TreeMap<>();
        // 替换为您的云 api 密钥 SecretId
        config.put("secretId", secretId);
        // 替换为您的云 api 密钥 SecretKey
        config.put("secretKey", secretKey);
        // 初始化 policy
        Policy policy = new Policy();
        // 临时密钥有效时长，单位是秒，默认 1800 秒，目前主账号最长 2 小时（即 7200 秒），子账号最长 36 小时（即 129600）秒
        config.put("durationSeconds", qCloudProperties.getDurationSeconds());
        // 换成您的 bucket
        config.put("bucket", bucketName);
        // 换成 bucket 所在地区
        config.put("region", qCloudProperties.getRegion());
        // 开始构建一条 statement
        Statement statement = new Statement();
        // 声明设置的结果是允许操作
        statement.setEffect("allow");
        /*
         * 密钥的权限列表。必须在这里指定本次临时密钥所需要的权限。
         * 权限列表请参见 https://cloud.tencent.com/document/product/436/31923
         * 规则为 {project}:{interfaceName}
         * project : 产品缩写  cos相关授权为值为cos,数据万象(数据处理)相关授权值为ci
         * 授权所有接口用*表示，例如 cos:*,ci:*
         * 添加一批操作权限 :
         */
        statement.addActions(new String[]{
                //上传对象
                "cos:PutObject",
                // 表单上传、小程序上传
                "cos:PostObject",
                // 初始化分块上传任务
                "cos:InitiateMultipartUpload",
                //查询分块上传任务
                "cos:ListMultipartUploads",
                //查询已上传的分块
                "cos:ListParts",
                //上传对象分块
                "cos:UploadPart",
                //完成分块上传任务
                "cos:CompleteMultipartUpload",
                //用于判断存储桶是否存在，存在时返回基本信息
                "cos:HeadBucket",
                //创建存储桶
                "cos:PutBucket",
                //列出存储桶中的对象
                "cos:GetBucket",
                //下载对象
                "cos:GetObject"
        });
        /*
         * 这里改成允许的路径前缀，可以根据自己网站的用户登录态判断允许上传的具体路径
         * 资源表达式规则分对象存储(cos)和数据万象(ci)两种
         * 数据处理、审核相关接口需要授予ci资源权限
         *  cos : qcs::cos:{region}:uid/{appid}:{bucket}/{path}
         *  ci  : qcs::ci:{region}:uid/{appid}:bucket/{bucket}/{path}
         * 列举几种典型的{path}授权场景：
         * 1、允许访问所有对象："*"
         * 2、允许访问指定的对象："a/a1.txt", "b/b1.txt"
         * 3、允许访问指定前缀的对象："a*", "a/*", "b/*"
         *  如果填写了“*”，将允许用户访问所有资源；除非业务需要，否则请按照最小权限原则授予用户相应的访问权限范围。
         * 示例：授权examplebucket-1250000000 bucket目录下的所有资源给cos和ci 授权两条Resource
         */
        statement.addResources(new String[]{
                "qcs::cos:" + qCloudProperties.getRegion() + ":uid/" + qCloudProperties.getAppId() + ":" + qCloudProperties.getBucketName() + "/*",
                "qcs::ci:" + qCloudProperties.getRegion() + ":uid/" + qCloudProperties.getAppId() + ":bucket/" + qCloudProperties.getBucketName() + "/*"});
        // 把一条 statement 添加到 policy
        // 可以添加多条
        policy.addStatement(statement);
        // 将 Policy 示例转化成 String，可以使用任何 json 转化方式，这里是本 SDK 自带的推荐方式
        config.put("policy", Jackson.toJsonPrettyString(policy));
        return CosStsClient.getCredential(config);
    }

    /**
     * 上传文件至OSS
     *
     * @param multipartFile 多部份文件
     * @param fileSuffix    文件后缀
     * @return 上传文件的结果
     */
    @Override
    public UploadResult upload(MultipartFile multipartFile, String fileSuffix) throws IOException {
        String originalFilename = multipartFile.getOriginalFilename();
        String suffix = FileUtil.getSuffix(originalFilename);
        COSClient cosClient = getCosClient();
        OssProperties.QCloudProperties qCloudProperties = ossProperties.getQCloud();
        String bucketName = qCloudProperties.getBucketName();
        boolean b = cosClient.doesBucketExist(bucketName);
        if (!b) {
            try {
                CreateBucketRequest createBucketRequest = new CreateBucketRequest(bucketName);
                // 设置 bucket 的权限为 Private(私有读写)、其他可选有 PublicRead（公有读私有写）、PublicReadWrite（公有读写）
                createBucketRequest.setCannedAcl(CannedAccessControlList.Private);
                //不存在则创建桶
                cosClient.createBucket(createBucketRequest);
            } catch (CosClientException e) {
                log.error("", e);
                throw new ServiceException("上传文件失败,请重试!");
            }
        }
        // 指定文件上传到 COS 上的路径，即对象键。例如对象键为 folder/picture.jpg，则表示将文件 picture.jpg 上传到 folder 路径下
        String key = getPath(qCloudProperties.getPrefix(), suffix);
        try (InputStream inputStream = multipartFile.getInputStream()) {
            ObjectMetadata metadata = new ObjectMetadata();
            metadata.setContentType(multipartFile.getContentType());
            PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, key, inputStream, metadata);
            cosClient.putObject(putObjectRequest);
            UploadResult uploadResult = new UploadResult(OssType.QCLOUD);
            uploadResult.setBucketName(bucketName);
            String url = qCloudProperties.getUrl();
            url = url.concat(StringPool.SLASH).concat(key);
            uploadResult.setUrl(url);
            uploadResult.setOssPath(key);
            String fileName = key.substring(key.lastIndexOf('/') + 1);
            uploadResult.setFileName(fileName);
            return uploadResult;
        } catch (Exception e) {
            log.error("", e);
            throw new ServiceException("文件上传失败,请重试!");
        } finally {
            try {
                cosClient.shutdown();
            } catch (Exception ignored) {
            }
        }
    }

    /**
     * 获取oss客户端
     *
     * @return oss客户端
     * @throws IOException io异常
     */
    private COSClient getCosClient() throws IOException {
        Response tempAuthorization = getTempAuthorization();
        Credentials credentials = tempAuthorization.credentials;
        String tmpSecretId = credentials.tmpSecretId;
        String tmpSecretKey = credentials.tmpSecretKey;
        String sessionToken = credentials.sessionToken;
        return getCosClient(tmpSecretId, tmpSecretKey, sessionToken);
    }

    /**
     * oss客户端构建
     *
     * @param tmpSecretId  临时id
     * @param tmpSecretKey 临时ket
     * @param sessionToken sessionToken
     * @return oss客户端
     */
    private COSClient getCosClient(String tmpSecretId, String tmpSecretKey, String sessionToken) {
        OssProperties.QCloudProperties qCloudProperties = ossProperties.getQCloud();
        BasicSessionCredentials cred = new BasicSessionCredentials(tmpSecretId, tmpSecretKey, sessionToken);
        // 2 设置 bucket 的地域
        // clientConfig 中包含了设置 region, https(默认 http), 超时, 代理等 set 方法, 使用可参见源码或者常见问题 Java SDK 部分
        //COS_REGION 参数：配置成存储桶 bucket 的实际地域，例如 ap-beijing，更多 COS 地域的简称请参见 https://cloud.tencent.com/document/product/436/6224
        Region region = new Region(qCloudProperties.getRegion());
        ClientConfig clientConfig = new ClientConfig(region);
        // 3 生成 cos 客户端
        return new COSClient(cred, clientConfig);
    }

    /**
     * 下载文件
     *
     * @param sysOss              文件上传记录
     * @param httpServletRequest  请求
     * @param httpServletResponse 响应
     * @return 响应体
     */
    @Override
    public ResponseEntity<StreamingResponseBody> download(SysOss sysOss, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {
        try {
            String originalName = sysOss.getOriginalName();
            COSClient cosClient = getCosClient();
            String bucketName = sysOss.getBucketName();
            String key = sysOss.getOssPath();
            String rangeHeader = ServletUtil.getHeader(httpServletRequest, HttpHeaders.RANGE, StandardCharsets.UTF_8);
            long startByte;
            long endByte;
            Long contentLength = sysOss.getFileSize();
            if (rangeHeader != null && rangeHeader.startsWith("bytes=")) {
                String[] ranges = rangeHeader.substring(6).split("-");
                startByte = Long.parseLong(ranges[0].trim());
                endByte = (ranges.length > 1 && !ranges[1].isEmpty()) ? Long.parseLong(ranges[1].trim()) : contentLength - 1;
            } else {
                startByte = 0;
                endByte = contentLength - 1;
            }
            // 确保范围有效
            endByte = Math.min(endByte, contentLength - 1);
            GetObjectRequest getObjectRequest = new GetObjectRequest(bucketName, key);
            getObjectRequest.setRange(startByte, endByte);
            COSObject cosObject = cosClient.getObject(getObjectRequest);
            ObjectMetadata objectMetadata = cosObject.getObjectMetadata();
            contentLength = objectMetadata.getContentLength();
            String contentType = objectMetadata.getContentType();
            HttpHeaders httpHeaders = new HttpHeaders();
            httpHeaders.setContentType(MediaType.parseMediaType(contentType));
            // 创建正确编码的文件名
            ContentDisposition contentDisposition = ContentDisposition
                    .builder("attachment")
                    .filename(originalName, StandardCharsets.UTF_8)
                    .build();
            httpHeaders.setContentDisposition(contentDisposition);
            //内容长度
            httpHeaders.setContentLength(contentLength);
            HttpStatus status = HttpStatus.OK;
            if (StringUtils.isNotNull(rangeHeader)) {
                //断点续传的位置
                List<HttpRange> ranges = new ArrayList<>();
                ranges.add(HttpRange.createByteRange(startByte, endByte));
                httpHeaders.setRange(ranges);
                status = HttpStatus.PARTIAL_CONTENT;
            }
            return ResponseEntity.status(status)
                    .headers(httpHeaders)
                    .body(outputStream -> {
                        try (COSObjectInputStream cosObjectInput = cosObject.getObjectContent();
                             BufferedInputStream bufferedInputStream = new BufferedInputStream(cosObjectInput)) {
                            IoUtil.copy(bufferedInputStream, outputStream);
                        } finally {
                            try {
                                cosClient.shutdown();
                            } catch (Exception ignored) {
                            }
                        }
                    });
        } catch (Exception e) {
            log.error("", e);
            httpServletResponse.setCharacterEncoding("utf-8");
            return ResponseEntity.internalServerError()
                    .contentType(MediaType.TEXT_PLAIN)
                    .body(o -> {
                        String errorMessage = "下载文件出现异常!";
                        byte[] bytes = errorMessage.getBytes(StandardCharsets.UTF_8);
                        o.write(bytes);
                    });
        }
    }
}
